<template>
    <!-- 页面容器 -->
    <div class="file">
        <!-- 页面标题 -->
        <h1>文件和文件夹上传</h1>
        <!-- 选择多个文件夹上传的输入框，webkitdirectory 支持选择文件夹，multiple 支持多选 -->
        多个文件夹上传<input type="file" @change="handleFolderUpload" webkitdirectory multiple>
        <!-- 选择单个或多个文件上传的输入框，multiple 支持多选 -->
        单个文件上传<input type="file" @change="handleFolderUpload" multiple>
        <!-- 当存在总分片数量时，显示进度条 -->
        <div v-if="totalChunks > 0" class="progress-bar">
            <!-- 当上传进度小于 100% 时，显示进度条 -->
            <div v-if="uploadProgress < 100" class="progress" :style="{ width: uploadProgress + '%' }"></div>
            <!-- 当上传进度小于 100% 时，显示当前上传进度的百分比 -->
            <span v-if="uploadProgress < 100">{{ uploadProgress }}%</span>
            <!-- 当上传进度达到 100% 时，显示上传成功 -->
            <span v-if="uploadProgress === 100">上传成功</span>
        </div>
    </div>
</template>

<script setup>
// 从 vue 导入 ref 和 computed 函数
import { ref, computed } from 'vue'
// 导入用于计算文件哈希值的库
import SparkMD5 from 'spark-md5'
// 导入用于发送 HTTP 请求的 axios 库
import axios from 'axios'

// 设置 axios 的基础请求地址
axios.defaults.baseURL = 'http://127.0.0.1:3000'
// 定义每个文件分片的大小为 1MB
const CHUNK_SIZE = 1024 * 1024 
// 用于存储每个文件的哈希值，键为文件名，值为哈希值
const fileHashes = ref({}) 
// 已上传的分片数量
const uploadedChunks = ref(0) 
// 总的分片数量
const totalChunks = ref(0) 
// 网络状态，初始值为在线
const networkStatus = ref('online') 
// 网络速度阈值，单位为 kbps
const networkSpeedThreshold = ref(500) 

// 文件分片函数，将文件分割成多个指定大小的分片
const createChunks = (file) => {
    const chunks = []
    let cur = 0
    // 循环分割文件，直到文件末尾
    while (cur < file.size) {
        const blob = file.slice(cur, cur + CHUNK_SIZE)
        chunks.push(blob)
        cur += CHUNK_SIZE
    }
    return chunks
}

// 计算文件哈希值的函数，通过读取文件的部分内容计算哈希值
const calculateHash = (chunks) => {
    return new Promise((resolve, reject) => {
        const targets = []
        const spark = new SparkMD5.ArrayBuffer()
        const fileReader = new FileReader()
        // 遍历文件分片
        chunks.forEach((chunk, index) => {
            if (index === 0 || index === chunks.length - 1) {
                // 第一个和最后一个分片全量参与计算
                targets.push(chunk)
            } else {
                // 其他分片取部分字节参与计算
                targets.push(chunk.slice(0, 2))
                targets.push(chunk.slice(CHUNK_SIZE / 2, CHUNK_SIZE / 2 + 2))
                targets.push(chunk.slice(CHUNK_SIZE - 2, CHUNK_SIZE))
            }
        })
        // 读取参与计算的内容
        fileReader.readAsArrayBuffer(new Blob(targets))
        // 读取完成后计算哈希值
        fileReader.onload = (e) => {
            spark.append(e.target.result)
            resolve(spark.end())
        }
    })
}

// 合并文件的请求函数，通知服务器合并已上传的分片
const mergeRequest = async (fileHash, fileName) => {
    let res = await axios.post('/merge', {
        fileHash,
        fileName,
        size: CHUNK_SIZE,
        headers: {
            "Content-Type": "application/json"
        },
    })
    // 如果服务器返回状态码为 200，表示合并成功
    if (res.data.code === 200) {
        console.log(`File ${fileName} uploaded successfully`)
    }
}

// 上传文件分片的函数
const uploadChunks = async (chunks, existChunks, fileHash, fileName) => {
    // 过滤掉已经存在的分片
    const data = chunks.filter((_, index) => !existChunks.includes(fileHash + '-' + index)).map((chunk, index) => ({
        fileHash,
        chunkHash: fileHash + '-' + index,
        chunk
    }))
    
    // 将需要上传的分片数据封装成 FormData 对象
    const formDatas = data.map((item) => {
        const formData = new FormData()
        formData.append('fileHash', item.fileHash)
        formData.append('chunkHash', item.chunkHash)
        formData.append('chunk', item.chunk)
        return formData
    })

    // 根据网络状态设置最大并发请求数
    const max = navigator.onLine ? (navigator.connection && navigator.connection.effectiveType === 'slow-2g' ? 1 : 6) : 1 
    let index = 0
    const taskPool = []

    // 循环上传分片
    while (index < formDatas.length) {
        const formData = formDatas[index]
        const task = axios.post('/upload', formData, {
            onUploadProgress: progressEvent => {
                // 计算当前分片的上传进度
                const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total)
                console.log(`Chunk ${index} upload progress: ${percentCompleted}%`)
                // 已上传分片数量加 1
                uploadedChunks.value += 1
            },
        })
        taskPool.push(task)
        // 当并发请求数达到最大时，等待一个请求完成
        if (taskPool.length >= max) {
            await Promise.race(taskPool)
            taskPool.shift()
        }
        index++
    }
    // 等待所有分片上传完成
    await Promise.all(taskPool)

    // 通知服务器合并文件
    await mergeRequest(fileHash, fileName)
}

// 校验文件是否已存在的函数
const verify = (fileHash, fileName) => {
    return axios.post('/verify', {
        fileHash,
        fileName,
        headers: {
            "Content-Type": "application/json"
        }
    }).then(res => res)
}

// 处理文件或文件夹上传的函数
const handleFolderUpload = async (e) => {
    // 将选择的文件列表转换为数组
    const files = Array.from(e.target.files)
    // 遍历每个文件
    for (const file of files) {
        // 对文件进行分片
        const chunks = createChunks(file)
        // 计算文件的哈希值
        const hash = await calculateHash(chunks)
        // 存储文件的哈希值
        fileHashes.value[file.name] = hash

        // 更新总分片数量
        totalChunks.value += chunks.length

        // 校验文件是否已存在
        const data = await verify(hash, file.name)
        if (data.data.data.shouldUpload === false) {
            console.log(`File ${file.name} already exists, skipping upload.`)
            continue
        }

        // 上传文件分片
        await uploadChunks(chunks, data.data.data.existChunks, hash, file.name)
    }
}

// 监听网络状态变化，当网络变为在线时更新状态
window.addEventListener('online', () => {
    networkStatus.value = 'online'
})

// 监听网络状态变化，当网络变为离线时更新状态
window.addEventListener('offline', () => {
    networkStatus.value = 'offline'
})

// 计算上传进度百分比的计算属性
const uploadProgress = computed(() => {
    return totalChunks.value > 0 ? Math.round((uploadedChunks.value / totalChunks.value) * 100) : 0
})
</script>

<style scoped>
.file {
    width: 50%;
    height: 60vh;
    background: gainsboro;
    margin: 20vh auto;
    text-align: center;
}

.progress-bar {
    width: 80%;
    margin: 20px auto;
    background-color: #f3f3f3;
    border-radius: 5px;
    overflow: hidden;
}

.progress {
    height: 30px;
    background-color: #4caf50;
}

span {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    color: white;
}
</style>